Skip to content

fix(runtime): avoid implicit computer-use images - #3585

Merged
Astro-Han merged 2 commits into
apache:mainfrom
hqhq1025:codex/computer-use-context-images
Aug 24, 2026
Merged

fix(runtime): avoid implicit computer-use images#3585
Astro-Han merged 2 commits into
apache:mainfrom
hqhq1025:codex/computer-use-context-images

Conversation

@hqhq1025

@hqhq1025 hqhq1025 commented Aug 23, 2026

Copy link
Copy Markdown
Contributor

Summary

  • keep automatically captured Computer Use frames available to the local PiP without sending them to the model for semantic actions
  • include model-visible image blocks only for explicit visual requests and legacy coordinate actions
  • preserve fresh accessibility observations for semantic actions such as element_sequence
  • bind the provider-image decision to the immutable invocation that actually executed
  • classify every canonical Computer Use action through an exhaustive typed screenshot policy

Root cause

Mutating Computer Use actions attach a final screenshot for the desktop PiP. toModelOutput previously projected every attached screenshot into the next provider request, so a semantic element_sequence result carried an unnecessary base64 PNG even though it already returned a fresh accessibility observation. The first revision also evaluated the image gate from settlement-time caller input and represented the action catalogue as an untyped parallel set. The current revision derives the decision from the frozen executed input and carries it on the result, while an exhaustive Record<CuToolActionType, ...> makes every action an explicit policy choice.

Measured result

A real Calculator run using element_sequence completed successfully. The final provider request dropped from 156,262 bytes to 73,727 bytes, contained no base64 string over 10 KB, and reduced the post-action input-token increase from 1,575 to 1,056 tokens.

Verification

  • built @maka/core, @maka/storage, and @maka/mcp in dependency order
  • npm --workspace @maka/runtime run typecheck
  • npm --workspace @maka/runtime run build
  • node --test packages/runtime/dist/__tests__/computer-use-tools.test.js packages/runtime/dist/__tests__/computer-use-schema-parity.test.js (89 passed)
  • npx biome check packages/runtime/src/computer-use-tools.ts packages/runtime/src/__tests__/computer-use-tools.test.ts
  • git diff --check origin/main...HEAD
  • real Electron Computer Use run against Calculator with a 12-step semantic element_sequence

AI use

Select exactly one:

  • No generative tool made a substantive contribution
  • Generative tooling made a substantive contribution

Tool(s) and scope: Codex contributed implementation, tests, review follow-up, and verification. Both affected commits include Generated-by: Codex trailers.

Checklist

  • Tests cover the change and fail without it
  • Lint, format, typecheck and the affected suites pass locally

Does this PR entail a change in behavior?

  • Yes — described under Summary above
  • No

@hqhq1025
hqhq1025 requested review from Astro-Han August 23, 2026 09:15

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed at exact head 99c27eb2be4563eefa6ccbe7d1d369d9488d2bcb.

Coverage: what decides whether a screenshot reaches the model, which paths this change moves from implicit-attach to no-attach, and whether anything that should still get a frame loses one. Not covered: wording.

The intent is right and the observable behaviour matches it: the decision is per tool call, not per turn or session; observe: true, screenshot, zoom, and the legacy coordinate actions still carry an image, while other semantic actions keep a fresh accessibility observation without the frame. Two problems in how that decision is made.

[P1] The image gate reads mutable caller input, not the frozen call it executed

packages/runtime/src/computer-use-tools.ts:2818-2832toModelOutput: ({ input, output }) calls shouldSendScreenshotToModel(input), and that input is the original caller-held object at settlement time, not the immutable snapshot the implementation actually ran with.

Reproduced: begin an async settlement with observe / include_screenshot: false; after impl has started, mutate the original object to true; the returned result carries the PiP screenshot and the provider modelOutput then contains a file block. The reverse mutation swallows an image the caller explicitly asked for. The PiP frame a semantic action carries can be reclassified the same way before settlement — which walks straight through the boundary this PR exists to establish.

Cloning inside the callback would already be too late. The fix is for toModelOutput to use the same verified immutable call, or for impl to emit an explicit model-image decision derived from the frozen input.

[P2] The action catalogue is untyped and parallel to the canonical union

computer-use-tools.ts:345-367 introduces MODEL_SCREENSHOT_ACTIONS as a ReadonlySet<string> sitting alongside CU_ACTION_TYPES / CU_TOOL_ACTION_TYPES, and the tests sample only five inputs.

The consequence is a silent default: a legitimately added action will simply not send an image, while every existing schema and parity test still passes. That is precisely the drift the comments near core warn about. Deriving the set exhaustively from the canonical action union — a typed map or derived set — and iterating every action in a classification test would make a new action a compile-time or test-time decision instead of a silent one.

Verification and limits

Built clean in dependency order (core → storage → mcp → runtime). Computer Use suites 261/261; Biome and git diff --check pass. Across the whole Runtime: 3064 pass, 1 fail, 13 skipped — the single failure is a macOS Bash executable-root environment assertion in untouched code, reproducible standalone, and hosted CI is green on this exact head.

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Incremental closure check at exact head 99c27eb2be4563eefa6ccbe7d1d369d9488d2bcb: the head has not changed since review 5002157364, so both previously reported blockers remain. Reposting them inline at their exact diff authorities; no APPROVE on this head.

Comment thread packages/runtime/src/computer-use-tools.ts Outdated
Comment thread packages/runtime/src/computer-use-tools.ts Outdated
@hqhq1025
hqhq1025 force-pushed the codex/computer-use-context-images branch from 99c27eb to e7205fa Compare August 24, 2026 07:46

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks — narrowing which Computer Use results carry an image into the model is worth doing, and test is green on e7205fa5.

I re-checked the two earlier points against this head. Both still apply; details are inline.

中文

感谢——收窄哪些 Computer Use 结果会把图片带进模型是值得做的,e7205fa5test 已绿。

之前两条我对着这个 head 重新核过,都仍然成立,具体见行内。

Comment thread packages/runtime/src/computer-use-tools.ts Outdated
Comment thread packages/runtime/src/computer-use-tools.ts Outdated
@hqhq1025
hqhq1025 force-pushed the codex/computer-use-context-images branch from e7205fa to 8e660ad Compare August 24, 2026 08:06

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The behaviour change is right and the test covers the interesting cases — semantic actions and observe without a picture stay text-only, while screenshot, an explicit include_screenshot, and the coordinate actions still get the image block.

One thing to fix before this merges.

[P2] MODEL_SCREENSHOT_ACTIONS restates CU_ACTION_TYPES by hand, and nothing holds the two together

The new set in packages/runtime/src/computer-use-tools.ts:345-360 is exactly CU_ACTION_TYPES minus cursor_position and wait. It is written out as fifteen string literals with no link back to the source list and no test comparing them.

That is the drift this area has already paid for once. packages/core/src/computer-use.ts:227-251 documents it directly: a hand-written list beside a schema that listed the same names fell behind when window_action was added, and the fix was to stop hand-writing it and add computer-use-schema-parity.test.ts to compare the lists in both directions. The comment also makes the point that matters here — drift where a name is missing is "just as costly and quieter."

The failure mode is worse than the original one, because it is silent in both directions. Add a coordinate action and it reaches the model with no picture, which looks like a model that cannot see rather than a missing list entry; no test fails. Remove or rename one and the dead entry never announces itself.

Deriving the set removes the class of bug rather than the instance:

const NON_VISUAL_COORDINATE_ACTIONS = new Set(['cursor_position', 'wait']);
const MODEL_SCREENSHOT_ACTIONS: ReadonlySet<string> = new Set(
  CU_ACTION_TYPES.filter((action) => !NON_VISUAL_COORDINATE_ACTIONS.has(action)),
);

Now adding a coordinate action opts it into vision by default, and deliberately withholding a picture is a one-line, reviewable edit to the exclusion set. If you would rather keep the explicit list, please extend computer-use-schema-parity.test.ts to assert this set against CU_ACTION_TYPES so drift fails a test instead of quietly blinding the model.

简体中文

行为改动是对的,测试也覆盖了关键分支:语义动作和不带图的 observe 只返回文本,而 screenshot、显式的 include_screenshot、以及坐标类动作仍然拿到图像块。

合并前有一处需要修。

[P2] MODEL_SCREENSHOT_ACTIONS 是手写重述 CU_ACTION_TYPES,且没有任何东西保证两者一致

packages/runtime/src/computer-use-tools.ts:345-360 新增的这个集合,正好等于 CU_ACTION_TYPES 去掉 cursor_positionwait。它被写成十五个字符串字面量,既没有回指来源列表,也没有测试比对两者。

这正是这块代码已经付过一次学费的漂移。 packages/core/src/computer-use.ts:227-251 明确记录了这件事:一份手写列表与一份列着相同名字的 schema 并存,结果在新增 window_action 时落后了;修法是不再手写,并加上 computer-use-schema-parity.test.ts 双向比对两份列表。那段注释还点出了此处真正要害的一句——少写一个名字的漂移代价一样大,而且更安静。

这里的失败模式比当初那次更糟,因为它两个方向都是静默的。新增一个坐标动作,它会带不到图就进模型,表现出来像是"模型看不见"而不是"列表少了一项",不会有任何测试失败;删掉或改名一个,多余项也不会自曝。

把集合推导出来,消除的是这一类 bug 而不是这一个实例:

const NON_VISUAL_COORDINATE_ACTIONS = new Set(['cursor_position', 'wait']);
const MODEL_SCREENSHOT_ACTIONS: ReadonlySet<string> = new Set(
  CU_ACTION_TYPES.filter((action) => !NON_VISUAL_COORDINATE_ACTIONS.has(action)),
);

这样新增坐标动作默认就进入视觉通道,而"刻意不给图"变成对排除集合的一行可审改动。如果你更想保留显式列表,请在 computer-use-schema-parity.test.ts 里补上这个集合与 CU_ACTION_TYPES 的断言,让漂移以测试失败的方式暴露,而不是悄悄把模型变成瞎子。

@hqhq1025
hqhq1025 force-pushed the codex/computer-use-context-images branch from 8e660ad to 4300288 Compare August 24, 2026 12:32
@hqhq1025
hqhq1025 requested review from Astro-Han August 24, 2026 12:32
@hqhq1025

Copy link
Copy Markdown
Contributor Author

@Astro-Han Thanks for the follow-up. I rechecked this concern against the current exact head 4300288c2838eaf01ac103c069b950937f3cbeee after rebasing onto the latest main.

The MODEL_SCREENSHOT_ACTIONS: ReadonlySet<string> described in the review no longer exists on this head. The current implementation is:

  • packages/runtime/src/computer-use-tools.ts:347-376: COMPUTER_USE_MODEL_SCREENSHOT_POLICY is an object literal checked with satisfies Record<CuToolActionType, 'always' | 'explicit' | 'never'>. A canonical action added without a policy entry is therefore a compile-time error; an unknown/dead policy key is also rejected. cursor_position and wait are deliberately and visibly classified as never.
  • packages/runtime/src/__tests__/computer-use-tools.test.ts:2632-2634: the regression compares all runtime policy keys with the complete canonical CU_TOOL_ACTION_TYPES list, so runtime/export drift fails the test.

I intentionally kept the exhaustive policy instead of defaulting every future coordinate action to vision through a derived exclusion set. Whether pixels may enter model context is a policy boundary, so a new action should require an explicit always, explicit, or never decision rather than inherit a permissive default.

Fresh verification on this head: Runtime typecheck and build pass; the Computer Use tool and schema-parity suites pass 89/89; Biome and git diff --check pass. No additional source change was needed for this follow-up because the current head already implements the requested typed exhaustive mapping and full canonical parity test.

Could you please re-review this head and approve if the evidence resolves the concern?

@Astro-Han Astro-Han left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The rewrite is better than what I asked for. Turning the set into COMPUTER_USE_MODEL_SCREENSHOT_POLICY keyed by every action, with satisfies Record<CuToolActionType, …>, makes the compiler reject a new action that nobody classified — that is stronger than the parity test I suggested, because it fails at build time rather than in a test run, and the added Object.keys(...) assertion still covers the ordering and the runtime shape. The three-way always / explicit / never split also reads better than the implicit "everything except two" it replaces.

Binding the decision to includeScreenshotInModelOutput on the executed result, instead of re-reading input in toModelOutput, closes a hole I had not raised: the input object is reachable and mutable after the call, so the old form let a later mutation change what the model was shown for an invocation that had already run. The regression test for exactly that is a good addition.

test is green on the current head and nothing is outstanding on my side. Approving.

简体中文

这次重写比我提的建议更好。把集合改成按每个动作列举的 COMPUTER_USE_MODEL_SCREENSHOT_POLICY,配合 satisfies Record<CuToolActionType, …>,等于让编译器直接拒绝"新增了动作却没人给它分类"的情况——这比我建议的对拍测试更强,因为它在构建期就失败,而不是等测试跑起来;同时新增的 Object.keys(...) 断言仍然覆盖了顺序和运行期形状。always / explicit / never 这个三分法,也比原来隐含的"除两个之外全都要"更好读。

把判断绑定到执行结果上的 includeScreenshotInModelOutput,而不是在 toModelOutput 里重新读 input,还堵上了一个我没有提出的问题:调用结束后 input 对象仍然可达且可变,原来的写法允许事后修改它,从而改变一次已经执行完的调用给模型看到的内容。针对这一点补的回归测试很到位。

当前 head 的 test 已通过,我这边没有遗留项。批准。

@Astro-Han

Copy link
Copy Markdown
Contributor

main is green at f37f5c3f29 and this is clean against it. Merging.

简体中文

mainf37f5c3f29 上是绿的,本分支相对它也是干净的。合并。

@Astro-Han
Astro-Han merged commit cba9e27 into apache:main Aug 24, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants